﻿using System;
using System.Linq;
using System.Threading;
using Microsoft.Scripting.Hosting;
using Microsoft.Linq.Expressions;
using Microsoft.Scripting.Hosting.Providers;
using Cjc.ExpressionEngine.Excel.Compiler;
using System.Collections.Generic;
using Cjc.ExpressionEngine.Excel.Runtime.Types;
using Microsoft.Scripting;
using Microsoft.Scripting.Runtime;
using Microsoft.Scripting.Ast;

namespace Cjc.ExpressionEngine
{
	public class Formula
	{
		private Engine engine;
		private string expressionText;
		private ScriptCode scriptCode;
		private IEnumerable<SymbolId> dependencies = null;

		internal Formula( Engine engine )
		{
			this.engine = engine;
		}

		public object Tag { get; set; }
		public string Format { get; set; }
		public object DebugValue { get; set; }

		public string ExpressionText
		{
			get { return expressionText; }
			set
			{
				expressionText = value;
				ScriptCode = null;
//				LambdaExpression = null;
			}
		}

		public Expression Expression
		{
			get { return ( ScriptCode != null ) ? scriptCode.Code.Body : null; }

			set
			{
				if ( value != null )
				{
					var builder = Utils.Lambda( typeof( object ), "Formula" );
					builder.Body = Expression.ConvertHelper( value, typeof( object ) );

					var sourceUnit = new SourceUnit(
						HostingHelpers.GetLanguageContext( engine.ScriptEngine ),
						null,
						null,
						SourceCodeKind.Expression );

					ScriptCode = new OptimizedScriptCode( builder.MakeLambda(), sourceUnit );
				}
				else ScriptCode = null;
			}
		}

		public Expression Parse( string expressionText )
		{
			return Parse( engine, expressionText );
		}

		public static Expression Parse( Engine engine, string expressionText )
		{
			return Compile( engine, expressionText ).Code;
		}

		internal static ScriptCode Compile( Engine engine, string expressionText )
		{
			var compiled = engine.ScriptEngine.CreateScriptSourceFromString(
				expressionText ).Compile();

			return HostingHelpers.GetScriptCode( compiled );
		}

/*
		internal LambdaExpression LambdaExpression
		{
			get
			{
				if ( lambdaExpression == null && expressionText != null )
				{
					Interlocked.CompareExchange<LambdaExpression>(
						ref lambdaExpression,
						ParseLambda( engine, expressionText ),
						null );
				}

				return lambdaExpression;
			}

			set
			{
				Interlocked.Exchange<LambdaExpression>( ref lambdaExpression, value );
				Interlocked.Exchange<DlrMainCallTarget>( ref target, null );
			}
		}
*/
		internal ScriptCode ScriptCode
		{
			get
			{
				if ( scriptCode == null && expressionText != null )
				{
					Interlocked.CompareExchange<ScriptCode>(
						ref scriptCode,
						Compile( engine, expressionText ),
						null );
				}

				return scriptCode;
			}

			set
			{
				Interlocked.Exchange<ScriptCode>( ref scriptCode, value );
			}
		}
/*
		internal DlrMainCallTarget Target
		{
			get
			{
				if ( target == null && LambdaExpression != null )
				{
					Interlocked.CompareExchange<DlrMainCallTarget>(
						ref target,
						lambdaExpression.Compile<DlrMainCallTarget>( true ),
						null );
				}

				return target;
			}

			set
			{
				Interlocked.Exchange<DlrMainCallTarget>( ref target, value );
			}
		}
*/
		public IEnumerable<SymbolId> Dependencies
		{
			get
			{
				if ( dependencies == null )
				{
					var expression = Expression;

					if ( expression != null )
					{
						var analyzer = new DependencyAnalyzer();
						analyzer.Visit( expression );

						Interlocked.Exchange<IEnumerable<SymbolId>>(
							ref dependencies,
							( from e in analyzer.Dependencies.OfType<ConstantExpression>()
							  select (SymbolId)e.Value ).ToList() );
					}
					else dependencies = new SymbolId[ 0 ];
				}

				return dependencies;
			}
		}

		internal object Execute( ScriptScope scope )
		{
			var scriptCode = ScriptCode;

			return ( scriptCode != null )
				? scriptCode.Run( HostingHelpers.GetScope( scope ) )
				: null;
		}
	}
}